﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;


namespace GraphicsEngineNamespace
{
    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// Klasa przygotowująca i rysująca teren.
    /// </summary>
    public class Terrain : Microsoft.Xna.Framework.DrawableGameComponent
    {
        #region Pola klasy Terrain
        private ContentManager content;

        public ContentManager Content
        {
            get { return content; }
            set { content = value; }
        }

        private GraphicsDevice graphicsDevice;

        private Effect effect;
        /// <summary>
        /// Technika stosowana do rysowania.
        /// </summary>
        private string technique = "Static";

        /// <summary>
        /// </summary>
        public string Technique
        {
            get { return technique; }
            set { technique = value; }
        }

        private VertexMultitextured[] vertices;
        private VertexBuffer myVertexBuffer;
        private int[] indices;
        private IndexBuffer myIndexBuffer;
        private VertexDeclaration myVertexDeclaration;

        /// <summary>
        /// Położenie terenu.
        /// </summary>
        private Vector3 location;

        public Vector3 Location
        {
            get { return location; }
            set { location = value; }
        }
        private int width;  // Szerokość terenu

        public int Width
        {
            get { return width; }
            set { width = value; }
        }
        private int length; // Długość terenu

        public int Length
        {
            get { return length; }
            set { length = value; }
        }

        private float[,] heightData;    // Wysokości 
        private float filedSize = 8.0f;    // Rozmiar pola [m]

        public float FiledSize
        {
            get { return filedSize; }
            set { filedSize = value; }
        }
        private float heightMax = 256.0f;  //maksymalna wysokośc terenu (skala terenu)

        /// <summary>
        /// Ścieżka do folderu z efektami.
        /// </summary>
        private static string effectFolderPath = "FX\\";
        /// <summary>
        /// Ścieżka do efektu.
        /// </summary>
        private string effectPath = effectFolderPath + "Phong";

        /// <summary>
        /// Ścieżka do folderu z teksturami.
        /// </summary>
        //private static string textureFolderPath = "Texture\\";
        /// <summary>
        /// Ścieżka do tekstury z wysokościami.
        /// </summary>
        private string textureHeightPath = /*textureFolderPath + */"default_heightmap";

        // Tekstury, maksymalnie 4
        // Ścieżki do tekstur.
        private string texturePath0 = "default_ambocc_map";
        private string texturePath1 = "default_ambocc_map";
        private string texturePath2 = "default_ambocc_map";
        private string texturePath3 = "default_ambocc_map";

        private string textureNormalPath0 = "default_normal_map";
        private string textureNormalPath1 = "default_normal_map";
        private string textureNormalPath2 = "default_normal_map";
        private string textureNormalPath3 = "default_normal_map";

        /*private string texturePath0 = textureFolderPath + "default_ambocc_map";
        private string texturePath1 = textureFolderPath + "default_ambocc_map";
        private string texturePath2 = textureFolderPath + "default_ambocc_map";
        private string texturePath3 = textureFolderPath + "default_ambocc_map";

        private string textureNormalPath0 = textureFolderPath + "default_normal_map";
        private string textureNormalPath1 = textureFolderPath + "default_normal_map";
        private string textureNormalPath2 = textureFolderPath + "default_normal_map";
        private string textureNormalPath3 = textureFolderPath + "default_normal_map";*/

        // Tekstury.
        /*private Texture2D texture0;
        private Texture2D texture1;
        private Texture2D texture2;
        private Texture2D texture3;

        // Normalne/Bump dla tekstur.
        private Texture2D textureNormal0;
        private Texture2D textureNormal1;
        private Texture2D textureNormal2;
        private Texture2D textureNormal3;*/

        /// <summary>
        /// Bump efekt.
        /// </summary>
        private bool normalMappingEffect = true;

        public bool NormalMappingEffect
        {
            get { return normalMappingEffect; }
            set { normalMappingEffect = value; }
        }
        #endregion

        #region Konstruktor
        public Terrain(Game game)
            : base(game)
        {
            // TODO: Construct any child components here
            Content = new ContentManager(game.Services);
            Content.RootDirectory = "Content";

            graphicsDevice = game.GraphicsDevice;
        }

        public Terrain(Game game, string textureHeightPath)
            : base(game)
        {
            // TODO: Construct any child components here
            Content = new ContentManager(game.Services);
            Content.RootDirectory = "Content";

            graphicsDevice = game.GraphicsDevice;

            this.textureHeightPath = textureHeightPath;
        }

        public Terrain(Game game, string textureHeightPath, float filedSize, float heightMax)
            : base(game)
        {
            // TODO: Construct any child components here
            Content = new ContentManager(game.Services);
            Content.RootDirectory = "Content";

            graphicsDevice = game.GraphicsDevice;

            this.textureHeightPath = textureHeightPath;
            this.filedSize = filedSize;
            this.heightMax = heightMax;
        }

        public Terrain(Game game, string textureHeightPath, float filedSize, float heightMax, 
            string texturePath0, string texturePath1, string texturePath2, string texturePath3)
            : base(game)
        {
            // TODO: Construct any child components here
            Content = new ContentManager(game.Services);
            Content.RootDirectory = "Content";

            graphicsDevice = game.GraphicsDevice;

            this.textureHeightPath = textureHeightPath;
            this.filedSize = filedSize;
            this.heightMax = heightMax;

            this.texturePath0 = texturePath0;
            this.texturePath1 = texturePath1;
            this.texturePath2 = texturePath2;
            this.texturePath3 = texturePath3;
        }

        public Terrain(Game game, string textureHeightPath, float filedSize, float heightMax, 
            string texturePath0, string texturePath1, string texturePath2, string texturePath3,
            string textureNormalPath0, string textureNormalPath1, string textureNormalPath2, 
                string textureNormalPath3)
            : base(game)
        {
            // TODO: Construct any child components here
            Content = new ContentManager(game.Services);
            Content.RootDirectory = "Content";

            graphicsDevice = game.GraphicsDevice;

            this.textureHeightPath = /*textureFolderPath + */textureHeightPath;
            this.filedSize = filedSize;
            this.heightMax = heightMax;

            this.texturePath0 = texturePath0;
            this.texturePath1 = texturePath1;
            this.texturePath2 = texturePath2;
            this.texturePath3 = texturePath3;

            this.textureNormalPath0 = textureNormalPath0;
            this.textureNormalPath1 = textureNormalPath1;
            this.textureNormalPath2 = textureNormalPath2;
            this.textureNormalPath3 = /*textureFolderPath + */textureNormalPath3;
        }
        #endregion

        #region Obliczenia i inne
        /// <summary>
        /// Zwraca wysokość terenu w danym punkcie
        /// </summary>
        /// <param name="xPos"></param>
        /// <param name="yPos"></param>
        /// <returns></returns>
        public float GetHeight(float xPos, float yPos)
        {
            xPos = xPos - Location.X * Width * FiledSize;
            yPos = -(yPos - Location.Y * Length * FiledSize);

            int x = (int)Math.Floor(xPos / filedSize);
            int y = (int)Math.Floor(yPos / filedSize);
            xPos /= filedSize;
            yPos /= filedSize;
            float dx = xPos - x;
            float dy = yPos - y;
            float p00 = heightData[x, y];

            float p01 = heightData[x, y + 1];

            float p10 = heightData[x + 1, y];

            float p11 = heightData[x + 1, y + 1];
            float h0 = p00 * (1 - dy) + p01 * dy;
            float h1 = p10 * (1 - dy) + p11 * dy;
            return h0 * (1 - dx) + h1 * dx;
        }

        /*public float GetHeight(int xFiledPos, int yFiledPos)
        {
            if (!(xFiledPos < Width))
                xFiledPos--;
            if (!(yFiledPos < Length))
                yFiledPos--;

            float xPos;
            float yPos;

            xPos = (float)xFiledPos * FiledSize;
            yPos = (float)yFiledPos * FiledSize;
            //yPos = -yPos;

            int x = xFiledPos;// (int)Math.Floor(xPos / filedSize);
            int y = yFiledPos;// (int)Math.Floor(yPos / filedSize);
            xPos /= filedSize;
            yPos /= filedSize;
            float dx = xPos - x;
            float dy = yPos - y;
            float p00 = heightData[x, y];

            int j = 0; if (y + 1 < Length) j = 1;
            float p01 = heightData[x, y + j];
            int i = 0; if (x + 1 < Width) i = 1;
            float p10 = heightData[x + i, y];

            float p11 = heightData[x + i, y + j];
            float h0 = p00 * (1 - dy) + p01 * dy;
            float h1 = p10 * (1 - dy) + p11 * dy;
            return h0 * (1 - dx) + h1 * dx;
        }*/

        /// <summary>
        /// Ustawienie współżędnych tekstur i wierzchołków.
        /// </summary>
        private void SetUpTextures()
        {
            /*int width = this.width + 1; ;
            int length = this.length + 1;
            vertices = new VertexMultitextured[width * length];

            // Texture coordinates
            float scale = (float)length / FiledSize; // Skala tekstur
            float tu = 0;   // Współżędne tekstury
            float tv = 0;   // Współżędne tekstury
            float tuDerivative = 1.0f / (length) * scale;
            float tvDerivative = 1.0f / (width) * scale;

            for (int x = 0; x < width; x++)
            {
                tu = 0.0f;
                for (int y = 0; y < length; y++)
                {
                    float z = GetHeight(x, y);
                    vertices[x + y * width].position = new Vector3(filedSize * x, z, 
                        filedSize * (-y));
                    vertices[x + y * width].textureCoordinate.X = tu;
                    vertices[x + y * width].textureCoordinate.Y = tv;
                    tu += tuDerivative;

                    // Teksktura 0 
                    vertices[x + y * width].texWeights.X = MathHelper.Clamp(
                        1.0f - Math.Abs(z - heightMax * 0.0f) / (heightMax * 0.5f), 0, 1);
                    // Tekstura 1
                    vertices[x + y * width].texWeights.Y = MathHelper.Clamp(
                        1.0f - Math.Abs(z - heightMax * 0.55f) / (heightMax * 0.2f), 0, 1);
                    // Tekstura 2
                    vertices[x + y * width].texWeights.Z = MathHelper.Clamp(
                        1.0f - Math.Abs(z - heightMax * 0.8f) / (heightMax * 0.2f), 0, 1);
                    // Tekstura 3
                    vertices[x + y * width].texWeights.W = MathHelper.Clamp(
                        1.0f - Math.Abs(z - heightMax) / (heightMax * 0.2f), 0, 1);

                    float total = vertices[x + y * width].texWeights.X;
                    total += vertices[x + y * width].texWeights.Y;
                    total += vertices[x + y * width].texWeights.Z;
                    total += vertices[x + y * width].texWeights.W;

                    vertices[x + y * width].texWeights.X /= total;
                    vertices[x + y * width].texWeights.Y /= total;
                    vertices[x + y * width].texWeights.Z /= total;
                    vertices[x + y * width].texWeights.W /= total;
                }
                tv += tvDerivative;
            }

            myVertexDeclaration = new VertexDeclaration(graphicsDevice,
                VertexMultitextured.vertexElements);*/

            vertices = new VertexMultitextured[width * length];

            // Texture coordinates
            float scale = (float)length / FiledSize * 8.0f; // Skala tekstur
            float tu = 0;   // Współżędne tekstury
            float tv = 0;   // Współżędne tekstury
            float tuDerivative = 1.0f / (length - 1) * scale;
            float tvDerivative = 1.0f / (width - 1) * scale;

            for (int x = 0; x < width; x++)
            {
                tu = 0.0f;
                for (int z = 0; z < length; z++)
                {
                    float y = heightData[x, z];

                    vertices[x + z * width].position = new Vector3(filedSize * x, y, 
                        filedSize * z);//(-z));
                    vertices[x + z * width].textureCoordinate.X = tu;
                    vertices[x + z * width].textureCoordinate.Y = tv;
                    tu += tuDerivative;

                    // Teksktura 0 
                    vertices[x + z * width].texWeights.X = MathHelper.Clamp(
                        1.0f - Math.Abs(y - heightMax * 0.0f) / (heightMax * 0.5f), 0, 1);
                    // Tekstura 1
                    vertices[x + z * width].texWeights.Y = MathHelper.Clamp(
                        1.0f - Math.Abs(y - heightMax * 0.55f) / (heightMax * 0.2f), 0, 1);
                    // Tekstura 2
                    vertices[x + z * width].texWeights.Z = MathHelper.Clamp(
                        1.0f - Math.Abs(y - heightMax * 0.8f) / (heightMax * 0.2f), 0, 1);
                    // Tekstura 3
                    vertices[x + z * width].texWeights.W = MathHelper.Clamp(
                        1.0f - Math.Abs(y - heightMax) / (heightMax * 0.2f), 0, 1);

                    float total = vertices[x + z * width].texWeights.X;
                    total += vertices[x + z * width].texWeights.Y;
                    total += vertices[x + z * width].texWeights.Z;
                    total += vertices[x + z * width].texWeights.W;

                    vertices[x + z * width].texWeights.X /= total;
                    vertices[x + z * width].texWeights.Y /= total;
                    vertices[x + z * width].texWeights.Z /= total;
                    vertices[x + z * width].texWeights.W /= total;
                }
                tv += tvDerivative;
            }

            myVertexDeclaration = new VertexDeclaration(graphicsDevice, 
                VertexMultitextured.vertexElements);
        }

        /// <summary>
        /// Ustawienie indeksów.
        /// </summary>
        private void SetUpIndices()
        {
            /*indices = new int[(width) * (length) * 6];
            int counter = 0;
            for (int y = 0; y < length; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    int lowerLeft = x + y * width;
                    int lowerRight = (x + 1) + y * width;
                    int topLeft = x + (y + 1) * width;
                    int topRight = (x + 1) + (y + 1) * width;

                    indices[counter++] = lowerLeft;
                    indices[counter++] = topLeft;
                    indices[counter++] = topRight;

                    indices[counter++] = lowerLeft;
                    indices[counter++] = topRight;
                    indices[counter++] = lowerRight;
                }
            }*/
            indices = new int[(width - 1) * (length - 1) * 6];
            int counter = 0;
            for (int y = 0; y < length - 1; y++)
            {
                for (int x = 0; x < width - 1; x++)
                {
                    int lowerLeft = x + y * width;
                    int lowerRight = (x + 1) + y * width;
                    int topLeft = x + (y + 1) * width;
                    int topRight = (x + 1) + (y + 1) * width;

                    /*indices[counter++] = lowerLeft;
                    indices[counter++] = topLeft;
                    indices[counter++] = topRight;

                    indices[counter++] = lowerLeft;
                    indices[counter++] = topRight;
                    indices[counter++] = lowerRight;*/

                    indices[counter++] = lowerLeft;
                    indices[counter++] = topRight;
                    indices[counter++] = topLeft;

                    indices[counter++] = lowerLeft;
                    indices[counter++] = lowerRight;
                    indices[counter++] = topRight;
                }
            }
        }

        /// <summary>
        /// Obliczenie wektorów normalnych.
        /// </summary>
        private void CalculateNormals()
        {
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].normal = new Vector3(0, 0, 0);

            for (int i = 0; i < indices.Length / 3; i++)
            {
                int index1 = indices[i * 3];
                int index2 = indices[i * 3 + 1];
                int index3 = indices[i * 3 + 2];

                Vector3 side1 = vertices[index1].position - vertices[index3].position;
                Vector3 side2 = vertices[index1].position - vertices[index2].position;
                Vector3 normal = Vector3.Cross(side1, side2);
                //Vector3 side1 = vertices[index1].position - vertices[index3].position;
                //Vector3 side2 = vertices[index1].position - vertices[index2].position;
                //Vector3 normal = Vector3.UnitY;//Vector3.Cross(side2, side1);

                vertices[index1].normal += normal;
                vertices[index2].normal += normal;
                vertices[index3].normal += normal;

                vertices[index1].tangent = vertices[index1].position - vertices[index2].position;
                vertices[index2].tangent = vertices[index2].position - vertices[index1].position;
                vertices[index3].tangent = vertices[index3].position - vertices[index1].position;  
            }
        }

        /// <summary>
        /// Załadowanie danych o wysokości z tekstury.
        /// </summary>
        /// <param name="heightMap"></param>
        private void LoadHeightData(Texture2D heightMap)
        {
            float minimumHeight = float.MaxValue;
            float maximumHeight = float.MinValue;

            int width = heightMap.Width+1;
            int length = heightMap.Height+1;

            Width = heightMap.Width;
            Length = heightMap.Height;
            Location = new Vector3((Location.X * Width) * FiledSize,
                    Location.Y, (Location.Z * Length) * FiledSize);

            Color[] heightMapColors = new Color[Width * Length];
            heightMap.GetData(heightMapColors);

            heightData = new float[width, length];
            for (int x = 0; x < Width; x++)
            {
                for (int y = 0; y < Length; y++)
                {
                    heightData[x, y] = heightMapColors[x + y * Width].R;
                    if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y];
                    if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y];
                }
            }

            for (int x = 0; x < width; x++)
            {
                heightData[x, Length] = heightData[x, Length-1];
            }
            for (int y = 0; y < length; y++)
            {
                heightData[Width, y] = heightData[Width-1, y];
            }
            Width++;
            Length++;

            /*float minimumHeight = float.MaxValue;
            float maximumHeight = float.MinValue;

            Width = heightMap.Width;
            Length = heightMap.Height;
            Location = new Vector3((Location.X * Width) * FiledSize,
                    Location.Y, (Location.Z * Length) * FiledSize);

            Color[] heightMapColors = new Color[width * length];
            heightMap.GetData(heightMapColors);

            heightData = new float[width, length];
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < length; y++)
                {
                    heightData[x, y] = heightMapColors[x + y * width].R;
                    if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y];
                    if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y];
                }
            }

            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < length; y++)
                {
                    heightData[x, y] = (short)((heightData[x, y] - minimumHeight)
                        / (maximumHeight - minimumHeight) * heightMax);
                }
            }*/
        }

        ///<summary>
        /// Pojedyńcze skopiowanie danych do karty graficznej żeby za każdym razem niekopiować 
        /// tych samych danych.
        /// </summary>
        private void CopyToBuffers()
        {
            myVertexBuffer = new VertexBuffer(graphicsDevice,
                vertices.Length * VertexMultitextured.sizeInBytes, BufferUsage.WriteOnly);
            myVertexBuffer.SetData(vertices);

            myIndexBuffer = new IndexBuffer(graphicsDevice, typeof(int), indices.Length,
                BufferUsage.WriteOnly);
            myIndexBuffer.SetData(indices);
        }
        #endregion

        #region Inicjacja
        /// <summary>
        /// Allows the game component to perform any initialization it needs to before starting
        /// to run.  This is where it can query for any required services and load content.
        /// </summary>
        public override void Initialize()
        {
            // TODO: Add your initialization code here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // TODO: use this.Content to load your game content here        
            //Texture2D heightMap = Content.Load<Texture2D>(TextureManager.GetTexture(textureHeightPath));
            LoadHeightData(TextureManager.GetTexture(textureHeightPath)/*heightMap*/);

            /*texture0 = Content.Load<Texture2D>(texturePath0);
            texture1 = Content.Load<Texture2D>(texturePath1);
            texture2 = Content.Load<Texture2D>(texturePath2);
            texture3 = Content.Load<Texture2D>(texturePath3);

            textureNormal0 = Content.Load<Texture2D>(textureNormalPath0);
            textureNormal1 = Content.Load<Texture2D>(textureNormalPath1);
            textureNormal2 = Content.Load<Texture2D>(textureNormalPath2);
            textureNormal3 = Content.Load<Texture2D>(textureNormalPath3);*/

            effect = content.Load<Effect>(effectPath);
            effect.Parameters["AmbientMap"].SetValue(TextureManager.GetTexture(texturePath2));
            //effect.Parameters["AlbedoMap"].SetValue(rockTexture);
            effect.Parameters["NormalMap"].SetValue(TextureManager.GetTexture(textureNormalPath2));

            SetUpTextures();
            SetUpIndices();
            CalculateNormals();
            CopyToBuffers();
        }
        #endregion
     
        #region Pozbycie się
        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
            Content.Unload();
        }
        #endregion

        #region Aktualizacja
        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            // TODO: Add your update code here

            base.Update(gameTime);
        }
        #endregion

        #region Rysowanie
        /// <summary>
        /// Rusowanie terenu.
        /// </summary>
        public void DrawTerrain(Matrix currentViewMatrix, Matrix currentProjectionMatrix,
            string technique)
        {
            //game.GraphicsDevice.RenderState.CullMode = CullMode.None;
            //GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;

            Matrix worldMatrix = Matrix.CreateTranslation(Location)
                //Matrix.Identity/*Matrix.CreateTranslation(-terrainWidth / 2.0f, 0, 
                 //terrainHeight / 2.0f)*/
                                        ;

            effect.CurrentTechnique = effect.Techniques[technique];
            effect.Parameters["sieye"].SetValue(new Vector4(
                        Camera.CameraPosition.X, Camera.CameraPosition.Y, Camera.CameraPosition.Z, 1.0f));
            //effect.Parameters["AmbientMap"].SetValue(texture2);
            //effect.Parameters["AlbedoMap"].SetValue(rockTexture);
            //effect.Parameters["NormalMap"].SetValue(textureNormal2);

            effect.Parameters["Model"].SetValue(worldMatrix);
            effect.Parameters["View"].SetValue( currentViewMatrix);
            effect.Parameters["Projection"].SetValue(currentProjectionMatrix);

            effect.Begin();
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Begin();

                graphicsDevice.VertexDeclaration = myVertexDeclaration;
                graphicsDevice.Indices = myIndexBuffer;
                graphicsDevice.Vertices[0].SetSource(myVertexBuffer, 0, 
                    VertexMultitextured.sizeInBytes);
                graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0,
                    vertices.Length, 0, indices.Length / 3);

                pass.End();
            }
            effect.End();

            //GraphicsDevice.RenderState.FogEnable = false;
            //game.GraphicsDevice.RenderState.FillMode = FillMode.Solid;
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        /*public override void Draw(GameTime gameTime)
        {
            DrawTerrain(Camera.viewMatrix, Camera.projectionMatrix, Technique);
        }*/
        #endregion
    }
}